/**
 * @license
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
(function(window) {
  'use strict';

  function GrAnnotationActionsInterface(plugin) {
    this.plugin = plugin;
    // Return this instance when there is an annotatediff event.
    plugin.on('annotatediff', this);

    // Collect all annotation layers instantiated by getLayer. Will be used when
    // notifying their listeners in the notify function.
    this._annotationLayers = [];

    // Default impl is a no-op.
    this._addLayerFunc = annotationActionsContext => {};
  }

  /**
   * Register a function to call to apply annotations. Plugins should use
   * GrAnnotationActionsContext.annotateRange and
   * GrAnnotationActionsContext.annotateLineNumber to apply a CSS class to the
   * line content or the line number.
   * @param {function(GrAnnotationActionsContext)} addLayerFunc The function
   *     that will be called when the AnnotationLayer is ready to annotate.
   */
  GrAnnotationActionsInterface.prototype.addLayer = function(addLayerFunc) {
    this._addLayerFunc = addLayerFunc;
    return this;
  };

  /**
   * The specified function will be called with a notify function for the plugin
   * to call when it has all required data for annotation. Optional.
   * @param {function(function(String, Number, Number, String))} notifyFunc See
   *     doc of the notify function below to see what it does.
   */
  GrAnnotationActionsInterface.prototype.addNotifier = function(notifyFunc) {
    // Register the notify function with the plugin's function.
    notifyFunc(this.notify.bind(this));
    return this;
  };

  /**
   * Returns a checkbox HTMLElement that can be used to toggle annotations
   * on/off. The checkbox will be initially disabled. Plugins should enable it
   * when data is ready and should add a click handler to toggle CSS on/off.
   *
   * Note1: Calling this method from multiple plugins will only work for the
   *        1st call. It will print an error message for all subsequent calls
   *        and will not invoke their onAttached functions.
   * Note2: This method will be deprecated and eventually removed when
   *        https://bugs.chromium.org/p/gerrit/issues/detail?id=8077 is
   *        implemented.
   *
   * @param {String} checkboxLabel Will be used as the label for the checkbox.
   *     Optional. "Enable" is used if this is not specified.
   * @param {function(HTMLElement)} onAttached The function that will be called
   *     when the checkbox is attached to the page.
   */
  GrAnnotationActionsInterface.prototype.enableToggleCheckbox = function(
      checkboxLabel, onAttached) {
    this.plugin.hook('annotation-toggler').onAttached(element => {
      if (!element.content.hidden) {
        console.error(
            element.content.id + ' is already enabled. Cannot re-enable.');
        return;
      }
      element.content.removeAttribute('hidden');

      const label = element.content.querySelector('#annotation-label');
      if (checkboxLabel) {
        label.textContent = checkboxLabel;
      } else {
        label.textContent = 'Enable';
      }
      const checkbox = element.content.querySelector('#annotation-checkbox');
      onAttached(checkbox);
    });
    return this;
  };

  /**
   * The notify function will call the listeners of all required annotation
   * layers. Intended to be called by the plugin when all required data for
   * annotation is available.
   * @param {String} path The file path whose listeners should be notified.
   * @param {Number} start The line where the update starts.
   * @param {Number} end The line where the update ends.
   * @param {String} side The side of the update ('left' or 'right').
   */
  GrAnnotationActionsInterface.prototype.notify = function(
      path, startRange, endRange, side) {
    for (const annotationLayer of this._annotationLayers) {
      // Notify only the annotation layer that is associated with the specified
      // path.
      if (annotationLayer._path === path) {
        annotationLayer.notifyListeners(startRange, endRange, side);
        break;
      }
    }
  };

  /**
   * Should be called to register annotation layers by the framework. Not
   * intended to be called by plugins.
   * @param {String} path The file path (eg: /COMMIT_MSG').
   * @param {String} changeNum The Gerrit change number.
   * @param {String} patchNum The Gerrit patch number.
   */
  GrAnnotationActionsInterface.prototype.getLayer = function(
      path, changeNum, patchNum) {
    const annotationLayer = new AnnotationLayer(path, changeNum, patchNum,
                                                this._addLayerFunc);
    this._annotationLayers.push(annotationLayer);
    return annotationLayer;
  };

  /**
   * Used to create an instance of the Annotation Layer interface.
   * @param {String} path The file path (eg: /COMMIT_MSG').
   * @param {String} changeNum The Gerrit change number.
   * @param {String} patchNum The Gerrit patch number.
   * @param {function(GrAnnotationActionsContext)} addLayerFunc The function
   *     that will be called when the AnnotationLayer is ready to annotate.
   */
  function AnnotationLayer(path, changeNum, patchNum, addLayerFunc) {
    this._path = path;
    this._changeNum = changeNum;
    this._patchNum = patchNum;
    this._addLayerFunc = addLayerFunc;

    this._listeners = [];
  }

  /**
   * Register a listener for layer updates.
   * @param {function(Number, Number, String)} fn The update handler function.
   *     Should accept as arguments the line numbers for the start and end of
   *     the update and the side as a string.
   */
  AnnotationLayer.prototype.addListener = function(fn) {
    this._listeners.push(fn);
  };

  /**
   * Layer method to add annotations to a line.
   * @param {HTMLElement} contentEl The DIV.contentText element of the line
   *     content to apply the annotation to using annotateRange.
   * @param {HTMLElement} lineNumberEl The TD element of the line number to
   *     apply the annotation to using annotateLineNumber.
   * @param {GrDiffLine} line The line object.
   */
  AnnotationLayer.prototype.annotate = function(contentEl, lineNumberEl, line) {
    const annotationActionsContext = new GrAnnotationActionsContext(
        contentEl, lineNumberEl, line, this._path, this._changeNum,
        this._patchNum);
    this._addLayerFunc(annotationActionsContext);
  };

  /**
   * Notify Layer listeners of changes to annotations.
   * @param {Number} start The line where the update starts.
   * @param {Number} end The line where the update ends.
   * @param {String} side The side of the update. ('left' or 'right')
   */
  AnnotationLayer.prototype.notifyListeners = function(
      startRange, endRange, side) {
    for (const listener of this._listeners) {
      listener(startRange, endRange, side);
    }
  };

  window.GrAnnotationActionsInterface = GrAnnotationActionsInterface;
})(window);
